package com.jj.service.room;

import com.jj.battle.protocol.GameProto;
import com.jj.entity.PlayerOperation;
import com.jj.entity.RecorderManager;
import com.jj.protocol.PlayerState;
import com.jj.protocol.RoomStatus;
import com.jj.service.entity.EntityImpl;
import com.jj.service.player.Player;
import lombok.Data;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.stream.Collectors;

/**
 * Copyrights: QidianGroup@JJ
 *
 * @author: LuHan
 * @date: 2018/1/15, 16:01
 */
public class RoomImpl extends EntityImpl implements Room{
    private static Logger logger = LoggerFactory.getLogger(RoomImpl.class);
    private static final int MAX_PLAYER_SIZE = 2;

    private HashMap<Long,Player> players = new HashMap<>();
    private RoomState roomState = RoomState.WAITING;
    //private BattleRecordManager battleRecordManager = new BattleRecordManager();
    private RecorderManager recorderManager = new RecorderManager();

    private long playerA, playerB;


    private UpdateThreader updateThreader = null;

    public volatile  boolean stopped = false;

    public void setPlayerA(long playerA) {
        this.playerA = playerA;
    }

    public void setPlayerB(long playerB) {
        this.playerB = playerB;
    }

    public RecorderManager getRecorderManager(){
        return recorderManager;
    }

    @Override
    public synchronized boolean join(Player player) {
        logger.debug("player {}, uid:{} try to join {}",
                player.hashCode(), player.getUserId(), getId());

        if(updateThreader == null){
            updateThreader =  new UpdateThreader();
            updateThreader.start();
        }

        try{
            if(players.containsKey(player.getUserId())){
                logger.debug("room {} already contains {}", getId(), player.getUserId());
                return true;
            }else if(players.size() <= MAX_PLAYER_SIZE){
                logger.debug("room {} add player {}", getId(), player.getUserId());
                players.put(player.getUserId(), player);
                return true;
            }else{
                logger.warn("Room {} already full while {} try to join",
                        getId(), player.getUserId());
                return false;
            }
        }finally {
            //人齐就开始广播
//            if(players.size() == MAX_PLAYER_SIZE){
//                tickerWorkerThread.addTicker(this);
//            }
        }
    }

    @Override
    public void leave(Player player) {
        leave(player.getUserId());
    }

    @Override
    public void leave(long uid) {
        logger.debug("player {} will leave", uid);
        players.remove(uid);
        if(players.size() == 0){
            logger.debug("room {} players size {}, try to stop", getId(), size());
            stop();
        }
    }


    @Override
    public void synState(int beginSeq, Player player) {
        GameProto.PKG_SYN_STATE_NTF.Builder ntfBuilder = GameProto.PKG_SYN_STATE_NTF.newBuilder();
        List<GameProto.PKG_FRAME> states = recorderManager.getSynState(beginSeq);
        ntfBuilder.addAllFrames(states).build();
        player.send(ntfBuilder.build());
    }

    @Override
    public RoomState getState() {
        return roomState;
    }

    @Override
    public synchronized void processCmd(Long uid, GameProto.PKG_OPERATION cmd) {
        //1. save operation
        recorderManager.addCmd(uid, cmd);
    }

    @Override
    public List<Player> getPlayers() {
        List<Player> list = new LinkedList<>();
        list.addAll(players.values());
        return list;
    }

    @Override
    public RoomStatus getStatus() {
        RoomStatus status = new RoomStatus();
        Iterator<Player> it = players.values().iterator();
        List<PlayerState> playerStates = new LinkedList<>();
        while(it.hasNext()){
            Player player = it.next();
            PlayerState playerState = new PlayerState();
            playerState.setUid(player.getUserId());
            playerState.setCharacterId(player.getCharacterId());
            playerStates.add(playerState);
        }
        status.setPlayers(playerStates);
        status.setSceneId("S04");
        return status;
    }

    @Override
    public int size() {
        return players.size();
    }

    @Override
    public long getPlayerA() {
        return playerA;
    }

    @Override
    public long getPlayerB() {
        return playerB;
    }

    @Override
    public void heartbeat() {

    }

    @Override
    public boolean needStop() {
        return stopped;
    }

    @Override
    public void stop() {
        stopped = true;
        if(updateThreader != null) {
            updateThreader.interrupt();
        }
    }

    @Override
    public GameProto.PKG_VERIFY buildVerify(){
        List<GameProto.PKG_FRAME>  frames = recorderManager.getSynState(0);
        GameProto.PKG_VERIFY.Builder verifyBuilder = GameProto.PKG_VERIFY.newBuilder();
        verifyBuilder.setPlayerId1(getPlayerA());
        verifyBuilder.setPlayerId2(getPlayerB());
        verifyBuilder.addAllAllFrames(frames);
        return verifyBuilder.build();
    }


    private static final Float FRAME_PER_SECOND = 60.0f;
    private static final Float interval = 1000.0f/FRAME_PER_SECOND;

    @Override
    public synchronized void tick() {
        if(recorderManager.getCurrentFrameNum() == -1){
            logger.debug("ignore empty frames");
        }else{
            GameProto.PKG_FRAME frame = recorderManager.getFrame(recorderManager.getCurrentFrameNum());
            if(frame.getOperationsList().size() > 0){
                logger.debug("contains action {} -> {}", recorderManager.getCurrentFrameNum(), frame.getOperationsList().size());
                List<PlayerOperation> ops = frame.getOperationsList().stream().
                        map(content -> PlayerOperation.fromMsg(content.getOperation(), content.getPlayerId())).collect(Collectors.toList());
                logger.debug("send frame:{}", ops);
            }else{
                logger.debug("empty frame");
            }
            logger.debug("room {} online players:{}", getId(), players.keySet());
            players.values().forEach(p -> p.send(frame));
        }

        //帧号增加
        recorderManager.increaseFrame();

        if(recorderManager.getCurrentFrameNum()>RecorderManager.MAX_FRAME){
            stopped = true;
            players.clear();
        }
    }

    private class UpdateThreader extends Thread{
        private volatile Integer _second = 0;
        private volatile Integer _cnt = 0;

        @Override
        public void run() {
            //这里是真正意义上的主循环，要记录时间
            //真正意义上的开始时间，0点
            //经过时间

            //用于记录游戏渲染的时间
            long update = 0;
            //用于记录游戏线程睡眠的时间
            long sleep = 0;

            //如果想要很好的中断游戏线程，可以把true变为自己规定的boolean变量，通过改变变量值中断游戏
            while (!stopped) {
                while (getSecondsCnt() >= FRAME_PER_SECOND.intValue()) {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        if(stopped){
                            return;
                        }
                    }
                }

                //得到当前时间点，注意nanoTime（）得到的是个时间点其数值并不是实际意义上的时间
                long before = System.nanoTime();
                //用于记录上一帧花费的时间
                long t = sleep + update;

                //游戏界面的更新和渲染，该方法是自己在主界面中定义的
                //begin process.
                //加空帧并广播出去
                if (!stopped) {
                    tick();
                } else {
                    return;
                }
                //end of process

                int cnt = getAndAddSeconds();
                update = (System.nanoTime() - before) / 1000000L;
                //这里是计算渲染的总共时间
                sleep = Math.max(2, (interval.intValue() - update));

                //因为我们规定了每一帧的睡眠时间是
                //16毫秒，所以在2和渲染后还剩余的时间中取个最大值作为睡眠时间，设2是为了防止渲染时间太长
                try {
                    Thread.sleep(sleep);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

        public Integer getSecnds(){
            return Calendar.getInstance().get(Calendar.SECOND);
        }

        public Integer getSecondsCnt(){
            Integer second = getSecnds();
            if(_second.equals(second)){
                return _cnt;
            }else{
                _second = second;
                _cnt = 0;
            }
            return _cnt;
        }

        public Integer getAndAddSeconds(){
            Integer second = getSecnds();
            if(_second.equals(second)){
                _cnt++;
            }else{
                _second = second;
                _cnt = 1;
            }
            return _cnt;
        }
    }
}
